package org.bud.test.base;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.bud.boot.Starter;
import org.bud.boot.constant.Constant;
import org.bud.boot.constant.Symbol;
import org.bud.util.bit.*;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.*;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.Filter;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

/**
 * @author shaun
 * @date 2024/7/19
 * @instruction
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Starter.class)
@Slf4j
public abstract class BaseTestCase {

    @Autowired
    private WebApplicationContext wac;

    protected Filter[] filters = new Filter[0];
    protected Map<String,Object> defaultHeaders;
    protected Map<String,Object> defaultRequestAttrs;
    protected Map<String,Object> defaultSessionAttrs;
    protected Predicate<JSONObject> defaultResultPredicate;
    protected String folder;

    private MockMvc mock;
    private Request request;
    private final boolean REPLACE_DEFAULT = true;

    @Before
    public void before(){
        this.mock = MockMvcBuilders.webAppContextSetup(wac)
                .addFilters(filters)
                .build();
        this.defaultResultPredicate =
                jsonObject -> Funcs.getOrThrow(
                        () -> jsonObject.getBoolean(Constant.SUCCESS));
        this.folder = "C:\\Users\\shaun\\Desktop\\download";
    }

    protected BaseTestCase request(String api,Map<String,Object> defaultParams){
        this.request = new Request().api(api).params(defaultParams);
        return this;
    }

    protected BaseTestCase headers(String api,Map<String,Object> defaultParams){
        this.request.headers(defaultParams);
        return this;
    }

    protected BaseTestCase requestAttrs(String api,Map<String,Object> defaultParams){
        this.request.requestAttrs(defaultParams);
        return this;
    }

    protected BaseTestCase sessionAttrs(String api,Map<String,Object> defaultParams){
        this.request.sessionAttrs(defaultParams);
        return this;
    }

    public boolean api() {
        this.request.handle(new ApiMock());
        return (this.request.resultPredicate.get());
    }

    public boolean stream(String file){
        this.request.handle(new StreamMock(file));
        return (this.request.resultPredicate.get());
    }

    protected Map<String,Object> map(Object... value){
        return Maps.newHashMap(value);
    }

    protected String genRandString(){
        return String.format("%s_%s",
                Dates.dateTime(LocalDateTime.now()).replaceAll("[:\\- ]",Symbol.BLANK),
                ThreadLocalRandom.current().nextInt(1000));
    }

    class Request{
        HttpMethod method;
        String api;
        Map<String,Object> headers;
        Map<String,Object> params;
        Map<String,Object> requestAttrs;
        Map<String,Object> sessionAttrs;
        MockHttpServletRequestBuilder builder;
        Predicate<JSONObject> resultPred;
        Supplier<Boolean> resultPredicate;
        Request api(final HttpMethod method){
            this.method = method;
            return this;
        }
        Request api(final String api){
            this.api = api;
            return this;
        }
        Request headers(final Map<String,Object> headers){
            this.headers = headers;
            return this;
        }
        Request params(final Map<String,Object> params){
            this.params = params;
            return this;
        }
        Request requestAttrs(final Map<String,Object> requestAttrs){
            this.requestAttrs = requestAttrs;
            return this;
        }
        Request sessionAttrs(final Map<String,Object> sessionAttrs){
            this.sessionAttrs = sessionAttrs;
            return this;
        }
        Request resultPredicate(Predicate<JSONObject> resultPred){
            this.resultPred = resultPred;
            return this;
        }
        Request resultPredicate(Supplier<Boolean> resultPredicate){
            this.resultPredicate = Optional.ofNullable(resultPredicate)
                    .orElse(() -> false);
            return this;
        }
        <T> UnaryOperator<T> sure(final T def,final boolean els){
            return m -> Optional.ofNullable(m).orElseGet(() -> els ? def : null);
        }
        void build(
                BiFunction<String,Object,MockHttpServletRequestBuilder> bi,Map<String,Object> map){
            build(bi,Object.class,map);
        }
        <T> void build(
                BiFunction<String,T,MockHttpServletRequestBuilder> bi,Class<T> clazz,Map<String,Object> map){
            map.entrySet().stream().forEach(
                    e -> Optional.ofNullable(e.getValue())
                          .ifPresent( a -> bi.apply(e.getKey(),
                                  Optional.ofNullable(a)
                                  .filter(o -> !clazz.equals(Object.class))
                                  .map(o -> (T) Optionals.ofNullable(o)
                                          .filter(i -> i instanceof String[])
                                          .map(String[].class::cast)
                                          .recover()
                                          .filter(Objects::nonNull)
                                          .map(Objects::toString)
                                          .map(i -> new String[]{i})
                                          .orElse(null)
                                  )
                                  .orElse((T)a))
                          ));
        }
        void sure(){
            this.headers = sure(defaultHeaders,REPLACE_DEFAULT).apply(headers);
            this.params = sure((Map<String,Object>)null,REPLACE_DEFAULT).apply(params);
            this.requestAttrs = sure(defaultRequestAttrs,REPLACE_DEFAULT).apply(requestAttrs);
            this.sessionAttrs = sure(defaultSessionAttrs,REPLACE_DEFAULT).apply(sessionAttrs);
            this.resultPred = sure(defaultResultPredicate,REPLACE_DEFAULT).apply(resultPred);
        }
        MockHttpServletRequestBuilder build(){
            if(Strings.isNotEmpty(api)){
                sure();
                method = Optional.ofNullable(method).orElse(HttpMethod.POST);
                if(method.equals(HttpMethod.GET)){
                    builder = MockMvcRequestBuilders.get(api);
                } else if(method.equals(HttpMethod.POST)){
                    builder = MockMvcRequestBuilders.post(api);
                }
                if(Maps.isNotEmpty(headers)){
                    build(builder::header,headers);
                }
                if(Maps.isNotEmpty(params)){
                    build(builder::param,String[].class,params);
                }
                if(Maps.isNotEmpty(requestAttrs)){
                    build(builder::requestAttr,requestAttrs);
                }
                if(Maps.isNotEmpty(sessionAttrs)){
                    build(builder::sessionAttr,sessionAttrs);
                }
            }
            return builder;
        }
        void handle(BaseMock mock){
            this.build();
            mock.request(this);
            this.resultPredicate(() -> mock.post(this));
        }
    }

    abstract class BaseMock {
        protected ResultActions perform =
                Funcs.getOrThrow(() -> mock.perform(request.build()));
        protected MvcResult mvc;

        protected BaseMock andExpect(ResultMatcher... matchers){
            Arrays.stream(matchers)
                    .forEach(o ->
                            Funcs.getOrThrow(() -> this.perform.andExpect(o)));
            return this;
        }

        protected BaseMock andDo(ResultHandler... handlers){
            Arrays.stream(handlers)
                    .forEach(o ->
                            Funcs.getOrThrow(() -> this.perform.andDo(o)));
            return this;
        }

        private BaseMock andReturn(){
            mvc = Funcs.getOrThrow(() -> this.perform.andReturn());
            return this;
        }

        protected void pre(Request request){

        }

        protected void execute(Request request){

        }

        protected boolean post(Request request){
            return true;
        }

        protected void request(Request request){
            andReturn();
            pre(request);
            execute(request);
        }
    }

    class ApiMock extends BaseMock{
        @Override
        protected void execute(Request request) {
            this.andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(
                            //修改response的contentType解决乱码问题
                            v -> Optional.ofNullable(v.getResponse())
                            .ifPresent(o -> o.setContentType(
                                    o.getContentType().split(Symbol.COMMA)[0] + "; charset=utf-8")),
                            //打印请求和响应
                            MockMvcResultHandlers.print()
                    );
        }
        //打印日志
        @Override
        protected boolean post(Request request) {
            String content = Funcs.getOrThrow(
                    () -> this.mvc.getResponse().getContentAsString());
            log.info(content);
            return request.resultPred.test(JSON.parseObject(content));
        }
    }
    interface StreamToWrite {
        OutputStream os() throws Exception;
        default void toWrite(byte[] b){
            OutputStream os = Funcs.getOrThrow(() -> os(),false);
            Funcs.doOrThrow(o -> o.write(b),os,false);
            Funcs.doOrThrow(o -> o.flush(),os,false);
            Funcs.doOrThrow(o -> o.close(),os,false);
        }
    }
    class StreamMock extends BaseMock {
        private String file;

        protected StreamMock(String file){
            this.file = file;
        }

        @Override
        protected void execute(Request request){
            this.andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(v ->
                        ((StreamToWrite) () ->
                                new BufferedOutputStream(new FileOutputStream(
                                        new File(folder,String.format("%sT%s",genRandString(),file)))))
                                .toWrite(v.getResponse().getContentAsByteArray())
                    );
        }
    }
}
